﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.IsolatedStorage;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows;
using Microsoft.Phone.Info;

namespace Agent.Messaging
{
  public class DetailedLogger
  {
    static readonly byte[] NEWLINE_BYTES = Encoding.UTF8.GetBytes(Environment.NewLine);
    const string CRASH_SENTINEL_SUFFIX = ".crash_marker";
    const int LOGGER_INTERNAL_ID = -1;
    const string LOGGER_INTERNAL_NAME = "$Log";
    const int INITIAL_LIST_SIZE = 25;
    const int INITIAL_FULLNAME_LENGTH = 20;
    const int INITIAL_LOG_LINE_LENGTH = 250;

    static bool isExceptionHandlerAdded = false;
    static bool isClosed = false;
    static int nextId = 0;
    static IsolatedStorageFileStream fileStream;

    public static string LogFileName { get; set; }
    public static bool AppendToExistingLog { get; set; }
    public static string NamePrefix { get; set; }

    public string Name { get; private set; }
    public int UniqueId { get; private set; }
    public bool AlwaysLogToDebugWindow { get; set; }

    public DetailedLogger(string name)
    {
      Name = name;
      lock (typeof(DetailedLogger))
      {
        UniqueId = nextId++;

        OpenLogFile(this);

        if (!isExceptionHandlerAdded)
        {
          Deployment.Current.Dispatcher.BeginInvoke(delegate { Application.Current.UnhandledException += App_UnhandledException; });
          isExceptionHandlerAdded = true;
        }

        isClosed = false;
      }

      Log("Startup");
    }

    static void OpenLogFile(DetailedLogger instance)
    {
      if (LogFileName != null && fileStream == null)
      {
        var isf = IsolatedStorageFile.GetUserStoreForApplication();

        FileMode mode = FileMode.Create;
        if (AppendToExistingLog == true && isf.FileExists(LogFileName))
          mode = FileMode.Append;

        fileStream = isf.OpenFile(LogFileName, mode, FileAccess.Write, FileShare.Read);
        if (instance != null)
          instance.WriteHeader(mode);

        if (isf.FileExists(LogFileName + CRASH_SENTINEL_SUFFIX))
          LogInternal("Crash sentinel exists; last session may have crashed", LoggerSeverity.Warning, true);
        else
          isf.CreateFile(LogFileName + CRASH_SENTINEL_SUFFIX).Close();
      }
    }

    static void App_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
    {
      try
      {
        LogInternal(e.ExceptionObject.Message, LoggerSeverity.Error, true);
      }
      catch { }
    }

    void WriteHeader(FileMode mode)
    {
      if (mode == FileMode.Create)
        WriteNewHeader();

      WriteNewSessionHeader();
    }

    void WriteNewSessionHeader()
    {
      LogInternal("New Log Session", LoggerSeverity.Trace, AlwaysLogToDebugWindow);

      LogInternal("OS version: " + Environment.OSVersion.ToString() + ", Total memory: " + DeviceStatus.DeviceTotalMemory.ToString("N0"), LoggerSeverity.Information, AlwaysLogToDebugWindow);

      if (Debugger.IsAttached)
        LogInternal("Debugger is attached", LoggerSeverity.Information, AlwaysLogToDebugWindow);
      else
        LogInternal("Debugger is not attached", LoggerSeverity.Information, AlwaysLogToDebugWindow);
    }

    void WriteNewHeader()
    {
      List<string> strings = new List<string>(INITIAL_LIST_SIZE);

      if (IncludeDateStamp)
        strings.Add("Date");

      if (IncludeTimeStamp)
      {
        strings.Add("Time");
        strings.Add("MS");
        strings.Add("Ticks");
      }

      if (IncludeTimeDelta)
        strings.Add("Delta");

      if (IncludeThreadId)
      {
        strings.Add("ThreadId");
        strings.Add("ThreadName");
      }

      if (IncludeFileName)
        strings.Add("Filename");

      if (IncludeName)
        strings.Add("Name");

      if (IncludeCaller)
      {
        strings.Add("Object");
        strings.Add("Member");
      }

      strings.Add("Severity");

      strings.Add("Message");

      if (IncludeCurrentMemory)
      {
        strings.Add("Current Mem");
        strings.Add("Delta Mem");
      }

      if (IncludeCurrentMemory)
        strings.Add("Peak Mem");

      LogWriter(AlwaysLogToDebugWindow, strings.ToArray());
    }

    static DetailedLogger()
    {
      LogFileName = null;
      Separator = "\t";
      DefaultSeverity = LoggerSeverity.Trace;
      NamePrefix = null;
      IncludeCaller = true;
      IncludeName = true;
      IncludeThreadId = true;
      IncludeDateStamp = true;
      IncludeTimeStamp = true;
      IncludeCurrentMemory = true;
      IncludePeakMemory = true;
      AppendToExistingLog = true;
      IncludeTimeDelta = true;
      IncludeFileName = true;
    }

    public static LoggerSeverity DefaultSeverity { get; set; }

    public static bool IncludeTimeStamp { get; set; }

    public static bool IncludeDateStamp { get; set; }

    public static bool IncludeThreadId { get; set; }

    public static bool IncludeCaller { get; set; }

    public static bool IncludeName { get; set; }

    public static string Separator { get; set; }

    public static bool IncludeCurrentMemory { get; set; }

    public static bool IncludePeakMemory { get; set; }

    public static bool IncludeTimeDelta { get; set; }

    public static bool IncludeFileName { get; set; }

    static void LogWriter(bool alwaysLogToDebugWindow, params string[] args)
    {
      if (args.Length == 0)
        return;

      StringBuilder sb = new StringBuilder(INITIAL_LOG_LINE_LENGTH);
      int len = args.Length;
      for (int i = 0; i < len; i++)
      {
        if (i != 0)
          sb.Append(Separator);

        sb.Append(args[i]);
      }

      // Whether to show on the debug window
      bool logToDebugWindow = true;

      if (fileStream != null)
      {
        byte[] bytes = Encoding.UTF8.GetBytes(sb.ToString());
        lock (fileStream)
        {
          if (isClosed)
            throw new InvalidOperationException("Logger has been closed");

          try
          {
            fileStream.Write(bytes, 0, bytes.Length);
            fileStream.Write(NEWLINE_BYTES, 0, NEWLINE_BYTES.Length);
            fileStream.Flush();

            // Successfully logged; no need for debug window unless
            // explicitly asked for it.
            logToDebugWindow = alwaysLogToDebugWindow;
          }
          catch { }
        }
      }

      // Only send to debug window if we need to
      if (logToDebugWindow)
        Debug.WriteLine(sb.ToString());
    }

    static void Log(string message, LoggerSeverity severity, string name, int uniqueId, int stackDepth, bool alwaysLogToDebugWindow)
    {
      List<string> strings = new List<string>(INITIAL_LIST_SIZE);

      DateTime now = DateTime.Now;
      if (IncludeDateStamp)
      {
        strings.Add(now.ToShortDateString());
      }

      if (IncludeTimeStamp)
      {
        strings.Add(now.ToLongTimeString());
        strings.Add(now.Millisecond.ToString());
        strings.Add(now.Ticks.ToString());
      }

      if (IncludeTimeDelta)
      {
        strings.Add((TrackTimeDelta(DateTime.Now.Ticks) / 10000).ToString("N0"));
      }

      if (IncludeThreadId)
      {
        strings.Add(Thread.CurrentThread.ManagedThreadId.ToString());
        strings.Add(Thread.CurrentThread.Name ?? "<no name>");
      }

      if (IncludeFileName)
      {
        if (string.IsNullOrEmpty(LogFileName))
          strings.Add("<None>");
        else
          strings.Add(LogFileName);
      }

      if (IncludeName)
      {
        StringBuilder fullName = new StringBuilder(INITIAL_FULLNAME_LENGTH);
        if (String.IsNullOrWhiteSpace(NamePrefix) != true)
          fullName.Append(String.Format("{0}_", NamePrefix));

        fullName.Append(name);

        if (uniqueId != LOGGER_INTERNAL_ID)
          fullName.Append(String.Format(" [{0}]", uniqueId));

        strings.Add(fullName.ToString());
      }

      if (IncludeCaller)
      {
        if (uniqueId == LOGGER_INTERNAL_ID)
        {
          strings.Add("");
          strings.Add("");
        }
        else
        {
          MethodBase mb = new StackFrame(stackDepth).GetMethod();
          strings.Add(mb.DeclaringType.Name);
          strings.Add(mb.Name);
        }
      }

      strings.Add(severity.ToString());

      strings.Add(message);

      if (IncludeCurrentMemory)
      {
        if (uniqueId == LOGGER_INTERNAL_ID)
        {
          strings.Add("");
          strings.Add("");
        }
        else
        {
          long currentMemory = DeviceStatus.ApplicationCurrentMemoryUsage;
          long memoryDelta = TrackMemoryDelta(currentMemory);
          strings.Add(currentMemory.ToString("N0"));
          strings.Add(memoryDelta.ToString("N0"));
        }
      }

      if (IncludePeakMemory)
      {
        if (uniqueId == LOGGER_INTERNAL_ID)
          strings.Add("");
        else
          strings.Add(DeviceStatus.ApplicationPeakMemoryUsage.ToString("N0"));
      }

      LogWriter(alwaysLogToDebugWindow, strings.ToArray());
    }

    static long lastTickValue = 0;
    static long TrackTimeDelta(long newTicks)
    {
      long result;

      lock (typeof(DetailedLogger))
      {
        // Don't log initial startup as a delta (outlier in analysis)
        if (lastTickValue <= 0)
          result = 0;
        else
          result = newTicks - lastTickValue;

        lastTickValue = newTicks;
      }

      return result;
    }

    static long lastMemoryValue;
    static long TrackMemoryDelta(long newMemory)
    {
      long result;

      lock (typeof(DetailedLogger))
      {
        // Don't log initial startup as a delta (outlier in analysis)
        if (lastMemoryValue <= 0)
          result = 0;
        else
          result = newMemory - lastMemoryValue;

        lastMemoryValue = newMemory;
      }

      return result;
    }

    public void Log(string message)
    {
      Log(message, DefaultSeverity, Name, UniqueId, 2, AlwaysLogToDebugWindow);
    }

    public void Log(string message, LoggerSeverity severity)
    {
      Log(message, severity, Name, UniqueId, 2, AlwaysLogToDebugWindow);
    }

    static void LogInternal(string message, LoggerSeverity severity, bool alwaysLogDebug)
    {
      Log(message, severity, LOGGER_INTERNAL_NAME, LOGGER_INTERNAL_ID, 2, alwaysLogDebug);
    }

    public static void Close()
    {
      if (isClosed)
        return;

      LogInternal("Log closing", LoggerSeverity.Trace, false);

      lock (typeof(DetailedLogger))
      {
        if (fileStream != null)
        {
          lock (fileStream)
          {
            fileStream.Close();
            fileStream = null;
          }
        }

        try
        {
          LogInternal("Removing crash sentinel", LoggerSeverity.Trace, false);
          var isf = IsolatedStorageFile.GetUserStoreForApplication();
          if (isf.FileExists(LogFileName + CRASH_SENTINEL_SUFFIX))
            isf.DeleteFile(LogFileName + CRASH_SENTINEL_SUFFIX);
        }
        catch { }

        isClosed = true;
      }
    }

    public static void DeleteLogFile()
    {
      if (String.IsNullOrEmpty(LogFileName))
        return;

      lock (typeof(DetailedLogger))
      {
        Close();

        var isf = IsolatedStorageFile.GetUserStoreForApplication();
        if (isf.FileExists(LogFileName))
          isf.DeleteFile(LogFileName);

        OpenLogFile(null);
      }
    }
  }
}
